package com.source3g.dubbo.config.spring;

import com.alibaba.dubbo.config.*;
import com.alibaba.dubbo.config.annotation.Reference;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.springframework.beans.BeanUtils;
import org.springframework.beans.BeansException;
import org.springframework.beans.TypeConverter;
import org.springframework.beans.factory.*;
import org.springframework.beans.factory.config.BeanPostProcessor;
import org.springframework.beans.factory.config.ConfigurableListableBeanFactory;
import org.springframework.beans.factory.config.DependencyDescriptor;
import org.springframework.context.ApplicationContext;
import org.springframework.context.ApplicationContextAware;
import org.springframework.core.BridgeMethodResolver;
import org.springframework.core.MethodParameter;
import org.springframework.core.Ordered;
import org.springframework.core.PriorityOrdered;
import org.springframework.core.annotation.AnnotatedElementUtils;
import org.springframework.core.annotation.AnnotationAttributes;
import org.springframework.util.ClassUtils;
import org.springframework.util.ReflectionUtils;

import java.beans.PropertyDescriptor;
import java.lang.reflect.*;
import java.util.*;
import java.util.concurrent.ConcurrentHashMap;

/**
 * Created by jianglin on 2017/3/31
 */
public class DubboReferenceBeanPostProcessor implements BeanPostProcessor, PriorityOrdered, BeanFactoryAware,
        ApplicationContextAware {
    protected final Log logger = LogFactory.getLog(getClass());
    private ConfigurableListableBeanFactory beanFactory;
    private ApplicationContext              applicationContext;
    private final Map<Type, Object> cacheOfDubboReference = new ConcurrentHashMap<Type, Object>();


    public int getOrder() {
        return Ordered.LOWEST_PRECEDENCE;
    }

    public void setBeanFactory(BeanFactory beanFactory) throws BeansException {
        if (!(beanFactory instanceof ConfigurableListableBeanFactory)) {
            throw new IllegalArgumentException(
                    "DubboReferenceBeanPostProcessor requires a ConfigurableListableBeanFactory");
        }
        this.beanFactory = (ConfigurableListableBeanFactory) beanFactory;
    }

    public Object postProcessBeforeInitialization(final Object bean, final String beanName) throws BeansException {
        final Class<?> beanClass = bean.getClass();
        Class<?> targetClass = bean.getClass();
        do {
            // 属性
            ReflectionUtils.doWithFields(targetClass, new ReflectionUtils.FieldCallback() {
                public void doWith(Field field) throws IllegalArgumentException, IllegalAccessException {
                    Reference reference = AnnotatedElementUtils.findMergedAnnotation(field, Reference.class);
                    if (reference == null) {
                        return;
                    }
                    Object value;
                    if ((value = cacheOfDubboReference.get(field.getType())) == null) {
                        value = refer(reference, field.getType());
                    }
                    if (value != null) {
                        cacheOfDubboReference.put(field.getType(), value);
                        if (!beanFactory.containsBean(field.getType().getName())) {
                            beanFactory.registerSingleton(field.getType().getName(), value);
                        }
                        ReflectionUtils.makeAccessible(field);
                        field.set(bean, value);
                    }

                }
            }, new ReflectionUtils.FieldFilter() {
                public boolean matches(Field field) {
                    return field.isAnnotationPresent(Reference.class);
                }
            });
            // 方法
            ReflectionUtils.doWithLocalMethods(targetClass, new ReflectionUtils.MethodCallback() {
                public void doWith(Method method) throws IllegalArgumentException, IllegalAccessException {
                    Method bridgedMethod = BridgeMethodResolver.findBridgedMethod(method);
                    if (!BridgeMethodResolver.isVisibilityBridgeMethodPair(method, bridgedMethod)) {
                        return;
                    }
                    AnnotationAttributes ann = AnnotatedElementUtils.getMergedAnnotationAttributes(bridgedMethod, Reference.class);
                    if (ann != null && method.equals(ClassUtils.getMostSpecificMethod(method, beanClass))) {
                        if (Modifier.isStatic(method.getModifiers())) {
                            if (logger.isWarnEnabled()) {
                                logger.warn("Reference annotation is not supported on static methods: " + method);
                            }
                            return;
                        }
                        if (method.getParameterTypes().length == 0) {
                            if (logger.isWarnEnabled()) {
                                logger.warn("Reference annotation should only be used on methods with parameters: " +
                                        method);
                            }
                        }
                        PropertyDescriptor pd = BeanUtils.findPropertyForMethod(bridgedMethod, beanClass);


                        Method md = method;
                        Reference reference = AnnotatedElementUtils.findMergedAnnotation(md, Reference.class);
                        Object value;
                        if ((value = cacheOfDubboReference.get(pd.getPropertyType())) == null) {
                            value = refer(reference, pd.getPropertyType());
                        }
                        if (value != null) {
                            cacheOfDubboReference.put(pd.getPropertyType(), value);
                            if (!beanFactory.containsBean(pd.getPropertyType().getName())) {
                                beanFactory.registerSingleton(pd.getPropertyType().getName(), value);
                            }
                        }
                        Object[] arguments;
                        Class<?>[] paramTypes = md.getParameterTypes();
                        arguments = new Object[paramTypes.length];
                        DependencyDescriptor[] descriptors = new DependencyDescriptor[paramTypes.length];
                        Set<String> autowiredBeans = new LinkedHashSet<String>(paramTypes.length);
                        TypeConverter typeConverter = beanFactory.getTypeConverter();
                        for (int i = 0; i < arguments.length; i++) {
                            MethodParameter methodParam = new MethodParameter(md, i);
                            DependencyDescriptor currDesc = new DependencyDescriptor(methodParam, true);
                            currDesc.setContainingClass(beanClass);
                            descriptors[i] = currDesc;
                            try {
                                Object arg = beanFactory.resolveDependency(currDesc, beanName, autowiredBeans, typeConverter);
                                arguments[i] = arg;
                            } catch (BeansException ex) {
                                throw new UnsatisfiedDependencyException(null, beanName, new InjectionPoint(methodParam), ex);
                            }
                        }
                        if (arguments != null) {
                            ReflectionUtils.makeAccessible(md);
                            try {
                                md.invoke(bean, arguments);
                            } catch (InvocationTargetException e) {
                                throw new BeanCreationException(
                                        "Injection of @Reference dependencies failed for class [" + beanClass + "]", e);
                            }
                        }
                    }

                }
            });
            targetClass = targetClass.getSuperclass();
        }
        while (targetClass != null && targetClass != Object.class);
        return bean;
    }

    public Object postProcessAfterInitialization(Object bean, String beanName) throws BeansException {
        return bean;
    }

    public void setApplicationContext(ApplicationContext applicationContext) throws BeansException {
        this.applicationContext = applicationContext;
    }

    private Object refer(Reference reference, Class<?> referenceClass) {
        String interfaceName;
        if (!"".equals(reference.interfaceName())) {
            interfaceName = reference.interfaceName();
        } else if (!void.class.equals(reference.interfaceClass())) {
            interfaceName = reference.interfaceClass().getName();
        } else if (referenceClass.isInterface()) {
            interfaceName = referenceClass.getName();
        } else {
            throw new IllegalStateException("The @Reference undefined interfaceClass or interfaceName, and the property type " + referenceClass
                    .getName() + " is not a interface.");
        }

        ReferenceBeanFactory<?> referenceBeanFactory = new ReferenceBeanFactory<Object>(reference);
        referenceBeanFactory.setApplicationContext(this.applicationContext);
        referenceBeanFactory.setInterface(interfaceName);
        if (void.class.equals(reference.interfaceClass())
                && "".equals(reference.interfaceName())
                && referenceClass.isInterface()) {
            referenceBeanFactory.setInterface(referenceClass);
        }
        if (reference.registry() != null && reference.registry().length > 0) {
            List<RegistryConfig> registryConfigs = new ArrayList<RegistryConfig>();
            for (String registryId : reference.registry()) {
                if (registryId != null && registryId.length() > 0) {
                    registryConfigs.add(beanFactory.getBean(registryId, RegistryConfig.class));
                }
            }
            referenceBeanFactory.setRegistries(registryConfigs);
        }
        if (reference.consumer() != null && reference.consumer().length() > 0) {
            referenceBeanFactory.setConsumer(beanFactory.getBean(reference.consumer(), ConsumerConfig.class));
        }
        if (reference.monitor() != null && reference.monitor().length() > 0) {
            referenceBeanFactory.setMonitor(beanFactory.getBean(reference.monitor(), MonitorConfig.class));
        }
        if (reference.application() != null && reference.application().length() > 0) {
            referenceBeanFactory.setApplication(beanFactory.getBean(reference.application(), ApplicationConfig.class));
        }
        if (reference.module() != null && reference.module().length() > 0) {
            referenceBeanFactory.setModule(beanFactory.getBean(reference.module(), ModuleConfig.class));
        }
        if (reference.consumer() != null && reference.consumer().length() > 0) {
            referenceBeanFactory.setConsumer(beanFactory.getBean(reference.consumer(), ConsumerConfig.class));
        }
        try {
            referenceBeanFactory.afterPropertiesSet();
        } catch (RuntimeException e) {
            throw e;
        } catch (Exception e) {
            throw new IllegalStateException(e.getMessage(), e);
        }
        return referenceBeanFactory.get();
    }
}
